Univalle


1. Introducción

1.1 Relevancia del Análisis

[PLACEHOLDER: Explica la relevancia de QQQ, su importancia en el mercado global, volatilidad e impacto para inversionistas. Incluir contexto del Nasdaq-100 ETF.]

3. Metodología

La metodología adoptada sigue el procedimiento estándar de modelamiento de series temporales propuesto en la literatura Box-Jenkins: se particiona el conjunto de datos en un conjunto de entrenamiento destinado a la estimación de parámetros y validación de modelos candidatos, y un conjunto de prueba para evaluar la capacidad predictiva fuera de muestra. Este diseño respeta la naturaleza secuencial de datos financieros, evitando la fuga de información que comprometería la validez de los pronósticos.


3.1 Base de Datos

La base de datos utilizada proviene de Yahoo Finance, plataforma reconocida globalmente para obtención de datos financieros históricos de alta calidad y frecuencia diaria. Se extrajo información del ETF Invesco QQQ Trust (símbolo: QQQ), el cual replica el índice Nasdaq-100 compuesto por las 100 empresas no financieras de mayor capitalización bursátil listadas en el mercado Nasdaq, con fuerte concentración en sector tecnológico (Apple, Microsoft, NVIDIA, Amazon, Alphabet, Tesla, Meta).

La extracción se realizó mediante la función getSymbols() del paquete quantmod de R, automatizando la descarga directa desde fuente oficial. El período de estudio abarca desde 7 de octubre de 2022 hasta 2 de diciembre de 2025, capturando 791 observaciones diarias de precios de cierre. Este período es particularmente informativo: incluye recuperación post-crisis 2022, impulso del rally de inteligencia artificial (2023-2025), cambios en política monetaria de la Reserva Federal, y volatilidad estructural del sector tecnológico.


3.2 Variable de Análisis

El dataset se centra en una única variable cuantitativa de interés: precio de cierre diario del ETF QQQ. Esta variable representa el último precio de negociación durante cada sesión bursátil y es la medida estándar en análisis técnico y modelización de series financieras.

La elección del precio de cierre se fundamenta en que: (i) refleja el consenso del mercado al finalizar cada sesión, incorporando información completa del día; (ii) es el referente para cálculo de rendimientos y valoración de portafolios; (iii) es utilizado en la mayoría de indicadores técnicos y modelos predictivos; (iv) su disponibilidad es consistente sin interrupciones en días hábiles.


3.3 Partición Temporal de Datos

En análisis de series temporales, la partición de datos debe respetar el orden cronológico, diferenciándose fundamentalmente de validación cruzada aleatoria. La estrategia implementada divide la serie en dos subconjuntos contiguos:

  • Conjunto de Entrenamiento: Comprende la mayoría de observaciones históricas (aproximadamente 95% de datos), utilizado para identificar parámetros óptimos del modelo ARIMA, estimar coeficientes y evaluar criterios de información.

  • Conjunto de Prueba: Representa período posterior no utilizado durante ajuste (aproximadamente 5% de datos), destinado a evaluar capacidad predictiva en escenario realista de pronóstico.

Esta metodología es esencial porque: (i) evita “fuga de información” (data leakage) donde datos futuros influirían en predicción del pasado; (ii) mantiene estructura cronológica respetando dependencias temporales inherentes a series financieras; (iii) simula condiciones reales donde pronósticos se generan sin conocimiento de valores posteriores; (iv) proporciona métricas objetivas de desempeño ex-post que validan capacidad predictiva genuina.


3.4 Marco Conceptual: Modelos ARIMA(p,d,q)

Los modelos ARIMA (AutoRegressive Integrated Moving Average), desarrollados por Box y Jenkins en 1970, constituyen metodología sistemática y robusta para análisis y pronóstico de series temporales. Se fundamentan en la idea de que el valor actual de una serie puede explicarse mediante: (i) sus valores históricos (componente autorregresivo), (ii) transformaciones que alcancen estacionariedad (componente integrador), o (iii) errores de pronóstico pasados (componente de media móvil).

A diferencia de modelos de regresión que requieren variables explicativas externas, los modelos ARIMA son univariados, extrayendo toda información predictiva de la propia historia de la serie. Esta característica los hace especialmente valiosos en contextos financieros donde no se dispone de predictores externos confiables, o donde el objetivo es capturar dinámicas intrínsecas de evolución temporal.

3.4.1 Componente Autorregresivo AR(p)

El modelo AR(p) pronostica la variable utilizando combinación lineal de sus valores pasados:

\[y_t = c + \phi_1 y_{t-1} + \phi_2 y_{t-2} + \cdots + \phi_p y_{t-p} + \varepsilon_t\]

donde: - \(y_t\) es el valor de la serie en tiempo \(t\) - \(c\) es constante (intercepto o drift) - \(\phi_1, \phi_2, \ldots, \phi_p\) son coeficientes autorregresivos que miden influencia de cada rezago - \(\varepsilon_t\) es término de error (ruido blanco) con \(E[\varepsilon_t]=0\) y varianza \(\sigma^2\) constante

El coeficiente \(\phi_1\) en AR(1) indica persistencia de la serie: valores cercanos a 1 implican alta persistencia (shocks tienen efectos duraderos), mientras que valores cercanos a 0 indican reversión rápida a media. Para que modelo sea estacionario, los coeficientes deben satisfacer restricciones específicas (raíces del polinomio característico fuera del círculo unitario).

La interpretación económica es que si una serie ha estado elevada en período anterior, tiende a permanecer elevada actualmente, capturando inercia o persistencia en comportamiento de variable.

3.4.2 Componente de Media Móvil MA(q)

El modelo MA(q) utiliza errores de pronóstico pasados en lugar de valores históricos de variable:

\[y_t = c + \varepsilon_t + \theta_1 \varepsilon_{t-1} + \theta_2 \varepsilon_{t-2} + \cdots + \theta_q \varepsilon_{t-q}\]

donde \(\theta_1, \ldots, \theta_q\) son coeficientes de media móvil.

La interpretación económica es que shocks o sorpresas en serie tienen efectos transitorios que se disipan gradualmente. Esto captura comportamientos donde precios regresan a nivel promedio tras perturbación, en lugar de permanecer desplazados permanentemente. En contextos financieros, MA modela la reversión a media que caracteriza a mercados relativamente eficientes en corto plazo.

3.4.3 Componente de Integración I(d)

Muchas series económicas y financieras no son estacionarias en forma original, presentando tendencias, medias cambiantes, o varianzas no constantes. Una serie estacionaria tiene propiedades estadísticas (media, varianza, autocovarianzas) invariantes en tiempo; una serie no-estacionaria exhibe comportamientos dependientes del momento observado.

El componente de integración aborda esto mediante diferenciación, transformación que calcula cambios entre observaciones consecutivas:

\[y'_t = y_t - y_{t-1} = \Delta y_t\]

Esta operación (primera diferencia) elimina tendencias lineales y estabiliza media. Si una diferencia es insuficiente para estacionariedad, se aplica segunda diferencia:

\[y''_t = y'_t - y'_{t-1} = \Delta^2 y_t\]

El parámetro \(d\) indica número de diferencias necesarias para que serie se vuelva estacionaria. Una serie que requiere \(d\) diferencias se denomina “integrada de orden \(d\)”, denotada I(d). En práctica, rara vez se requieren más de dos diferencias; para series financieras como precios de activos, típicamente \(d=1\) es suficiente.

3.4.4 Formulación Compacta con Operador de Rezago

Combinando los tres componentes, el modelo ARIMA(p,d,q) se expresa compactamente usando operador de rezago \(B\) (donde \(B y_t = y_{t-1}\)):

\[\Phi(B)(1-B)^d y_t = c + \Theta(B) \varepsilon_t\]

donde: - \(\Phi(B) = 1 - \phi_1 B - \phi_2 B^2 - \cdots - \phi_p B^p\) es polinomio autorregresivo - \((1-B)^d\) es operador de diferenciación aplicado \(d\) veces - \(\Theta(B) = 1 + \theta_1 B + \theta_2 B^2 + \cdots + \theta_q B^q\) es polinomio de media móvil

Esta representación facilita análisis teórico y derivación de propiedades estadísticas. Su elegancia radica en capturar dinámicas complejas mediante combinación sistemática de componentes que actúan en niveles distintos: AR modela persistencia, MA modela efectos de shocks transitorios, I transforma serie hacia propiedades deseadas.

3.4.5 Supuestos Fundamentales del Modelo

Para que inferencias y pronósticos derivados de ARIMA sean válidos y confiables, deben cumplirse supuestos sobre naturaleza de serie y sus residuos:

  1. Estacionariedad: Serie temporal debe presentar estacionariedad en media y varianza, i.e., propiedades estadísticas permanecen aproximadamente constantes en tiempo. Esto asegura que relaciones identificadas entre observaciones son estables y predecibles, en lugar de resultado de tendencias determinísticas no modeladas.

  2. Ruido Blanco en Residuos: Los residuos del modelo ajustado deben comportarse como secuencia aleatoria e independiente sin patrones sistemáticos. Si residuos contienen estructura, sugiere que modelo no ha capturado completamente dinámica temporal, requiriendo respecificación.

  3. Homocedasticidad: Se asume que errores tienen varianza constante a través del tiempo. Violaciones (heteroscedasticidad o volatilidad que cambia en tiempo) comprometen precisión de intervalos de confianza.

  4. Normalidad de Residuos: Aunque no es estrictamente necesaria para estimación del modelo, la normalidad es deseable para construcción precisa de intervalos de confianza y validez de pruebas de hipótesis sobre parámetros.


3.5 Herramientas Estadísticas para Identificación y Validación

3.5.1 Prueba Aumentada de Dickey-Fuller (ADF)

La estacionariedad es requisito previo fundamental antes de modelización ARIMA. Una serie no-estacionaria produciría inferencias engañosas y pronósticos poco confiables.

La Prueba Aumentada de Dickey-Fuller (ADF) es contraste estadístico estándar para evaluación formal. Contrasta: - \(H_0\): Serie tiene raíz unitaria (no-estacionaria) - \(H_1\): Serie es estacionaria

El resultado de esta prueba determina el orden de integración \(d\): si serie original rechaza \(H_0\) (p-valor < 0.05), se procede a diferenciar y repetir hasta lograr estacionariedad. El estadístico ADF compara contra valores críticos tabulados; si estadístico de prueba es más negativo que valor crítico (mayor en magnitud), se rechaza \(H_0\).

3.5.2 Funciones de Autocorrelación: ACF y PACF

Las funciones de autocorrelación proporcionan diagnóstico visual fundamental para entender patrones de dependencia temporal y sugerir especificaciones ARIMA iniciales.

Función de Autocorrelación (ACF): Cuantifica correlación lineal entre observaciones separadas por rezagos 1, 2, …, k. Su comportamiento es diagnóstico: - En series estacionarias: ACF decae gradualmente hacia cero - En series no-estacionarias: ACF persiste en valores altos durante muchos rezagos

Para identificación de modelo, ACF es especialmente útil para distinguir procesos MA(q): un corte abrupto después de rezago \(q\) (i.e., autocorrelaciones significativas hasta lag \(q\) luego caen a cero dentro de bandas) sugiere componente MA(q).

Función de Autocorrelación Parcial (PACF): Elimina influencia de rezagos intermedios, aislando efecto directo de cada rezago sobre presente. Es especialmente informativa para identificar procesos AR(p): un corte abrupto después de rezago \(p\) sugiere componente AR(p).

El análisis combinado de ACF y PACF proporciona indicios valiosos sobre especificación inicial del modelo, aunque debe complementarse con criterios estadísticos formales.

3.5.3 Búsqueda Automática: Función auto.arima()

En práctica moderna, la función auto.arima() automatiza gran parte del proceso de identificación mediante algoritmos de búsqueda sistemática. Realiza búsqueda sobre rango especificado de valores para \(p\), \(d\) y \(q\), evaluando cada combinación mediante criterios de información como AICc.

Sin embargo, es importante reconocer limitaciones: un modelo automatizado puede omitir especificaciones que, aunque menos óptimas según criterios puramente estadísticos, podrían ser preferibles bajo consideraciones teóricas o interpretativas. Por ello, resulta valioso complementar búsqueda automática con análisis visual de ACF/PACF, permitiendo que teoría económica y conocimiento del dominio del problema informen especificación final.

3.5.4 Criterios de Selección del Modelo: AICc

Cuando múltiples especificaciones ARIMA son candidatas, es necesario criterio objetivo para elegir entre ellas. Los criterios de información cumplen este rol al balancear calidad del ajuste con complejidad del modelo.

El AICc (Criterio de Información de Akaike Corregido) es criterio más utilizado en práctica para selección de modelos ARIMA. Evalúa verosimilitud del modelo (qué tan bien se ajusta a datos) y aplica penalización por agregar parámetros adicionales, reduciendo tendencia al sobreajuste. Su fórmula es:

\[\text{AICc} = \text{AIC} + \frac{2(p+q+k+1)(p+q+k+2)}{T-p-q-k-2}\]

donde: - \(T\) es número de observaciones - \(p\) y \(q\) son órdenes del modelo ARIMA - \(k\) es número de parámetros adicionales (como constante)

El término adicional respecto al AIC clásico corrige sesgo que ocurre en muestras pequeñas. Especificaciones con menor AICc son preferibles, indicando mejor balance entre ajuste y parsimonia.

Aspecto crítico: Criterios de información NO son aplicables para seleccionar orden de diferenciación \(d\), pues diferenciación altera escala de datos sobre cual se computa verosimilitud, haciendo valores de AICc no comparables entre modelos con diferente \(d\). Por ello, parámetro \(d\) se determina primero mediante pruebas de estacionariedad (ADF), y AICc se utiliza posteriormente para optimizar órdenes \(p\) y \(q\).

3.5.5 Diagnóstico de Residuos: Prueba de Ljung-Box

Después de ajustar modelo ARIMA específico, es necesario verificar que modelo haya capturado adecuadamente estructura temporal.

La Prueba de Ljung-Box es contraste estadístico que evalúa si existe autocorrelación significativa en residuos. Contrasta: - \(H_0\): Residuos son ruido blanco independiente (sin autocorrelación) - \(H_1\): Residuos contienen autocorrelación significativa

Si residuos conservan autocorrelación, es señal de que modelo especificado no ha extraído completamente información temporal disponible, requiriendo respecificación con órdenes más altos. Un p-valor elevado (> 0.05) indica que residuos se comportan consistentemente con ruido blanco, suministrando confianza en que modelo ha cumplido su propósito de modelar adecuadamente dinámica temporal de serie.


3.6 Procedimiento Metodológico: Metodología Box-Jenkins

La metodología Box-Jenkins propone proceso iterativo sistemático para modelización ARIMA. El procedimiento implementado en este análisis sigue estos pasos secuenciales:

Paso 1: Determinación del Orden de Integración (d)

Se evalúa estacionariedad de serie mediante pruebas formales (ADF) y análisis visual de ACF. Se identifica número de diferencias necesarias para que serie se vuelva estacionaria: - Si serie original es estacionaria: \(d=0\) - Si primera diferencia es estacionaria: \(d=1\) - Si segunda diferencia es estacionaria: \(d=2\)

El parámetro \(d\) es determinado independientemente de \(p\) y \(q\), antes de cualquier comparación de criterios de información.

Paso 2: Análisis Exploratorio de ACF y PACF

Se examinan gráficos de autocorrelación para serie potencialmente diferenciada, obteniendo sugerencias preliminares sobre órdenes \(p\) y \(q\): - Si ACF corta abruptamente en lag \(q\): sugiere MA(q) - Si PACF corta abruptamente en lag \(p\): sugiere AR(p) - Si ambos decaen gradualmente: sugiere proceso mixto ARMA(p,q)

Paso 3: Identificación de Modelos Candidatos

Se identifican especificaciones ARIMA plausibles basadas en: - Análisis visual de ACF/PACF - Búsqueda automática mediante auto.arima() - Consideraciones de parsimonia (preferir modelos simples) - Teoría económica/financiera sobre comportamiento de variable

Se ajustan múltiples modelos candidatos con diferentes combinaciones de \((p,d,q)\).

Paso 4: Estimación Comparativa y Selección

Se comparan especificaciones mediante: - Criterios de información (AICc, AIC, BIC) - Métricas de precisión en datos de entrenamiento (RMSE, MAE, MAPE) - Principio de parsimonia: preferir modelos más simples si diferencias en ajuste son marginales

Se selecciona modelo que balancéa mejor ajuste con complejidad.

Paso 5: Validación Diagnóstica de Residuos

Se examinan residuos del modelo seleccionado mediante: - Análisis gráfico temporal de residuos (verificar media cero, varianza constante) - Gráfico de ACF de residuos (verificar ausencia de autocorrelación) - Q-Q plot (verificar aproximación a normalidad) - Prueba de Ljung-Box (verificar formalmente que residuos son ruido blanco)

Si residuos no se comportan como ruido blanco, se regresa a Paso 3 para respecificación.

Paso 6: Generación de Pronósticos

Una vez validado modelo, se procede a: - Pronósticos puntuales sobre conjunto de prueba - Intervalos de confianza (típicamente 95%) que cuantifican incertidumbre - Evaluación ex-post comparando predicciones contra valores reales observados


3.7 Métricas de Evaluación del Desempeño Predictivo

Para evaluar objetivamente precisión de pronósticos generados por modelo ARIMA en conjunto de prueba, se emplean métricas cuantitativas estándar:

RMSE (Root Mean Squared Error):

\[\text{RMSE} = \sqrt{\frac{1}{n} \sum_{t=1}^{n} (\hat{y}_t - y_t)^2}\]

Amplifica penalizaciones sobre errores grandes, siendo sensible a presencia de valores atípicos en errores de pronóstico. Útil para identificar si modelo comete errores sistemáticamente grandes en algún período.

MAE (Mean Absolute Error):

\[\text{MAE} = \frac{1}{n} \sum_{t=1}^{n} |\hat{y}_t - y_t|\]

Proporciona medida de error promedio menos sensible a outliers que RMSE, ofreciendo perspectiva más robusta de desempeño general. Tiene interpretación directa: en promedio, predicciones desviación esta cantidad del valor real.

MAPE (Mean Absolute Percentage Error):

\[\text{MAPE} = \frac{100}{n} \sum_{t=1}^{n} \left| \frac{y_t - \hat{y}_t}{y_t} \right|\]

Expresa error como porcentaje relativo del valor observado, permitiendo comparabilidad del desempeño independientemente de magnitud absoluta de valores predichos. Especialmente útil en contextos financieros donde escala de precios varía.

Cobertura de Intervalos de Confianza:

Se evalúa proporción de valores reales que caen dentro de intervalos de confianza predichos (típicamente 95%). Una calibración correcta sugiere que incertidumbre fue estimada apropiadamente; demasiadas observaciones fuera del intervalo sugiere que modelo subestimó volatilidad.


2. Descripción de la Serie Temporal

2.1 Contexto Histórico y Datos

[PLACEHOLDER: Contexto completo del QQQ (Nasdaq-100 ETF), período de análisis seleccionado (octubre 2022 - presente), eventos significativos que han afectado el precio (crisis de volatilidad, cambios de política monetaria, rally de IA, etc.)]

serie_QQQ <- getSymbols("QQQ", src="yahoo", auto.assign=FALSE, from="2015-01-01") 
Precio <- serie_QQQ$`QQQ.Close`

Gráfico Dinámico de la Serie

datos_qqq <- data.frame(
  Fecha = index(Precio),
  Precio = as.numeric(Precio)
)

datos_qqq <- datos_qqq %>%
  mutate(Corte = as.yearqtr(Fecha)) 

lista_frames <- lapply(unique(datos_qqq$Corte), function(c) {
  dt <- datos_qqq[datos_qqq$Corte <= c, ]
  dt$Frame <- as.character(c) 
  return(dt)
})

datos_animados <- dplyr::bind_rows(lista_frames)

p <- ggplot(datos_animados, aes(x = Fecha, y = Precio)) +
  geom_area(aes(frame = Frame), fill = qqq_pal$primary, alpha = 0.1, position = "identity") +
  geom_line(aes(frame = Frame), color = qqq_pal$primary, size = 0.8) +
  labs(
    title = "Evolución Dinámica del QQQ",
    subtitle = "Crecimiento histórico acumulado desde octubre 2022",
    x = "", 
    y = "Precio (USD)"
  ) +
  scale_y_continuous(labels = scales::dollar_format()) +
  theme_QQQ() +
  theme(plot.title = element_text(size = 14))

plotly::ggplotly(p, tooltip = c("x", "y")) %>%
  plotly::layout(
    paper_bgcolor = 'rgba(0,0,0,0)',
    plot_bgcolor = 'rgba(0,0,0,0)',
    font = list(family = "Inter, sans-serif", color = qqq_pal$text_gray),
    hovermode = "x unified"
  ) %>%
  plotly::animation_opts(frame = 100, transition = 0, redraw = FALSE) %>%
  plotly::animation_slider(currentvalue = list(prefix = "Período: ")) %>%
  plotly::config(displayModeBar = FALSE)

2.2 Estadísticas Descriptivas

[PLACEHOLDER: Estadísticas completas de la serie - Media, Mediana, Desv. Est., Rango, Cuartiles. Tablas formateadas con kableExtra.]


4. Resultados del Modelo ARIMA

4.1 Partición de Datos

4.1.1 Estrategia Train/Test

La aplicación de la estrategia de partición temporal al conjunto de datos del QQQ divide el período de análisis (octubre 2022 - presente) en dos subconjuntos contiguos que respetan el orden cronológico. El punto de división se establece en el 30 de septiembre de 2025, coincidiendo con el cierre del tercer trimestre del año 2025. Esta fecha marca un quiebre administrativo natural en calendarios financieros y evita arbitrariedades en la selección del período de corte.

El conjunto de entrenamiento comprende observaciones desde el 7 de octubre de 2022 hasta el 30 de septiembre de 2025, proporcionando una base robusta para estimación de coeficientes \(ARIMA\), mientras que el conjunto de prueba abarca desde el 1 de octubre de 2025 hasta el presente, permitiendo validación con un horizonte de pronóstico consistente con estándares de análisis de corto plazo en mercados financieros (Hyndman & Athanasopoulos, 2021).

Entrenamiento <- window(Precio, start = "2022-10-07", end="2025-09-30")
Prueba <- window(Precio, start = "2025-10-01")

4.1.2 Tabla Resumen: Observaciones por Conjunto

particion_resumen <- data.frame(
  Conjunto = c("Entrenamiento", "Prueba", "Total"),
  Período = c(
    "07-Oct-2022 → 30-Sep-2025",
    "01-Oct-2025 → Presente",
    "07-Oct-2022 → Presente"
  ),
  `Observaciones` = c(
    length(Entrenamiento),
    length(Prueba),
    length(Entrenamiento) + length(Prueba)
  ),
  Porcentaje = c(
    paste0(round(length(Entrenamiento)/(length(Entrenamiento)+length(Prueba))*100, 1), "%"),
    paste0(round(length(Prueba)/(length(Entrenamiento)+length(Prueba))*100, 1), "%"),
    "100%"
  ),
  Propósito = c(
    "Estimación y validación de modelo",
    "Evaluación de capacidad predictiva",
    ""
  )
)

kable(particion_resumen,
      caption = "Resumen de Partición de Datos: Entrenamiento vs Prueba",
      align = c("l", "c", "c", "c", "l")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, color = qqq_pal$primary) %>%
  column_spec(3, bold = TRUE, color = qqq_pal$positive) %>%
  row_spec(3, bold = TRUE, background = "#e8f5e9", color = qqq_pal$text_dark)
Resumen de Partición de Datos: Entrenamiento vs Prueba
Conjunto Período Observaciones Porcentaje Propósito
Entrenamiento 07-Oct-2022 → 30-Sep-2025 747 94.3% Estimación y validación de modelo
Prueba 01-Oct-2025 → Presente 45 5.7% Evaluación de capacidad predictiva
Total 07-Oct-2022 → Presente 792 100%

4.1.3 Visualización: Serie con Partición

df_train <- data.frame(
  Fecha = index(Entrenamiento),
  Precio = as.numeric(Entrenamiento),
  Conjunto = "Entrenamiento"
)

df_test <- data.frame(
  Fecha = index(Prueba),
  Precio = as.numeric(Prueba),
  Conjunto = "Prueba"
)

df_completo <- bind_rows(df_train, df_test)
fecha_corte <- as.Date("2025-10-01")

ggplot(df_completo, aes(x = Fecha, y = Precio)) +
  geom_ribbon(data = df_train, 
              aes(ymin = min(df_completo$Precio) * 0.95, ymax = Precio),
              fill = qqq_pal$primary, alpha = 0.08) +
  geom_ribbon(data = df_test, 
              aes(ymin = min(df_completo$Precio) * 0.95, ymax = Precio),
              fill = qqq_pal$secondary, alpha = 0.15) +
  geom_line(data = df_train, color = qqq_pal$primary, linewidth = 0.9) +
  geom_line(data = df_test, color = qqq_pal$secondary, linewidth = 1.1) +
  geom_vline(xintercept = fecha_corte, 
             linetype = "dashed", color = qqq_pal$negative, linewidth = 0.8) +
  annotate("text", x = fecha_corte, y = max(df_completo$Precio) * 1.02,
           label = "Corte: 01-Oct-2025", hjust = -0.05, vjust = 0,
           color = qqq_pal$negative, fontface = "bold", size = 3.5) +
  annotate("label", 
           x = as.Date("2024-01-01"), 
           y = max(df_completo$Precio) * 0.85,
           label = paste0("ENTRENAMIENTO\n", nrow(df_train), " observaciones"),
           fill = qqq_pal$primary, color = "white", 
           fontface = "bold", size = 3.5, label.padding = unit(0.5, "lines")) +
  annotate("label", 
           x = max(df_test$Fecha) - 10,
           y = min(df_completo$Precio) * 1.15,
           label = paste0("PRUEBA\n", nrow(df_test), " obs."),
           fill = qqq_pal$secondary, color = "white", 
           fontface = "bold", size = 3.2, label.padding = unit(0.4, "lines")) +
  scale_x_date(date_breaks = "4 months", date_labels = "%b %Y",
               expand = expansion(mult = c(0.02, 0.05))) +
  scale_y_continuous(labels = dollar_format(prefix = "$"),
                     expand = expansion(mult = c(0.05, 0.08))) +
  labs(
    title = "Partición de Datos: Entrenamiento vs Prueba",
    subtitle = "QQQ (Nasdaq-100 ETF) | Serie de precios de cierre diarios",
    x = NULL,
    y = "Precio de Cierre (USD)",
    caption = paste0("Fuente: Yahoo Finance | Período: ", 
                     min(df_completo$Fecha), " a ", max(df_completo$Fecha))
  ) +
  theme_QQQ() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

La partición temporal establecida asegura cumplimiento con el requisito académico de mínimo 60 períodos de entrenamiento—el conjunto cuenta con 746 observaciones diarias (aproximadamente 3 años de negociación). El conjunto de prueba, con 40+ observaciones, proporciona suficiente horizonte para validar los 10 pronósticos solicitados con márgenes de seguridad estadística. La partición es no aleatoria y respeta el orden temporal, preservando autocorrelaciones y dinámicas de corto plazo que caracterizan a series financieras.


4.2 Análisis de Estacionariedad

4.2.1 Serie en Niveles: Identificación de No-Estacionariedad

La serie de precios del QQQ exhibe visualmente una tendencia alcista pronunciada con fluctuaciones amplias alrededor de una trayectoria creciente durante el período de entrenamiento (octubre 2022 - septiembre 2025). El gráfico de autocorrelación de la serie en niveles revela el síntoma clásico de no-estacionariedad:

acf_data <- acf(Entrenamiento, lag.max = 30, plot = FALSE)

df_acf <- data.frame(
  Lag = acf_data$lag[-1], 
  ACF = acf_data$acf[-1]
)

n <- length(Entrenamiento)
limite_sup <- qnorm(0.975) / sqrt(n)
limite_inf <- -limite_sup

ggplot(df_acf, aes(x = Lag, y = ACF)) +
  geom_segment(aes(xend = Lag, yend = 0), 
               color = qqq_pal$primary, linewidth = 0.8) +
  geom_point(color = qqq_pal$primary, size = 2) +
  geom_hline(yintercept = limite_sup, linetype = "dashed", 
             color = qqq_pal$secondary, linewidth = 0.7) +
  geom_hline(yintercept = limite_inf, linetype = "dashed", 
             color = qqq_pal$secondary, linewidth = 0.7) +
  geom_hline(yintercept = 0, color = qqq_pal$text_gray, linewidth = 0.5) +
  annotate("rect", xmin = -Inf, xmax = Inf, 
           ymin = limite_inf, ymax = limite_sup,
           fill = qqq_pal$secondary, alpha = 0.1) +
  annotate("label", x = 20, y = 0.5,
           label = "Decaimiento lento\n→ Serie NO estacionaria",
           fill = qqq_pal$negative, color = "white",
           fontface = "bold", size = 3.5, label.padding = unit(0.5, "lines")) +
  scale_x_continuous(breaks = seq(0, 30, 5)) +
  scale_y_continuous(limits = c(-0.1, 1.05), breaks = seq(0, 1, 0.25)) +
  labs(
    title = "Función de Autocorrelación (ACF) - Serie en Niveles",
    subtitle = "QQQ: Precio de cierre | Datos de entrenamiento",
    x = "Rezago (Lag)",
    y = "Autocorrelación",
    caption = "Bandas azules: Límites de significancia al 95%"
  ) +
  theme_QQQ()

La autocorrelación muestral permanece elevada (>0.85) incluso en rezagos distantes (lag 30). Este decaimiento lento es el indicador clásico de que la serie contiene raíz unitaria y requiere diferenciación (Brockwell & Davis, 2016). Adicionalmente, observamos que casi todos los rezagos caen fuera de las bandas de confianza, confirmando correlación sistemática estructural en la serie.

4.2.2 Prueba: Test de Dickey-Fuller Aumentado (ADF)

adf_resultado <- adf.test(Entrenamiento)

tabla_adf <- data.frame(
  Métrica = c("Estadístico Dickey-Fuller", 
              "Orden de Rezagos (Lag)", 
              "P-valor",
              "Nivel de Significancia (α)",
              "Hipótesis Nula (H₀)",
              "Decisión"),
  Valor = c(round(adf_resultado$statistic, 4),
            adf_resultado$parameter,
            round(adf_resultado$p.value, 4),
            "0.05",
            "Serie tiene raíz unitaria",
            ifelse(adf_resultado$p.value > 0.05, 
                   "No rechazar H₀", "Rechazar H₀")),
  Interpretación = c("Valor del estadístico de prueba",
                     "Rezagos incluidos en el test",
                     "Probabilidad bajo H₀",
                     "Umbral de decisión",
                     "La serie NO es estacionaria",
                     ifelse(adf_resultado$p.value > 0.05,
                            "Serie NO estacionaria",
                            "Serie estacionaria ✓"))
)

kable(tabla_adf, 
      caption = "Prueba de Dickey-Fuller Aumentada (ADF) - Serie en Niveles",
      align = c("l", "c", "l")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  row_spec(3, bold = TRUE, color = qqq_pal$negative, background = "#ffe8e0") %>% 
  row_spec(6, bold = TRUE, background = "#fef3f2", color = qqq_pal$text_dark)
Prueba de Dickey-Fuller Aumentada (ADF) - Serie en Niveles
Métrica Valor Interpretación
Estadístico Dickey-Fuller -3.0468 Valor del estadístico de prueba
Orden de Rezagos (Lag) 9 Rezagos incluidos en el test
P-valor 0.1352 Probabilidad bajo H₀
Nivel de Significancia (α) 0.05 Umbral de decisión
Hipótesis Nula (H₀) Serie tiene raíz unitaria La serie NO es estacionaria
Decisión No rechazar H₀ Serie NO estacionaria

El estadístico ADF de -3.0468 es mayor (menos negativo) que el valor crítico aproximado de -3.43 para significancia al 5%. Con p-valor de 0.1352 > 0.05, no se rechaza \(H_0\): la serie de precios en niveles es no-estacionaria (DickeyFuller, 1979). Esta evidencia estadística justifica la aplicación de diferenciación.

4.2.3 Aplicación de Diferenciación de Primer Orden

dif_Entrenamiento <- diff(Entrenamiento) %>% na.omit()
df_diff <- data.frame(
  Fecha = index(dif_Entrenamiento),
  Valor = as.numeric(dif_Entrenamiento)
)

ggplot(df_diff, aes(x = Fecha, y = Valor)) +
  geom_line(color = qqq_pal$secondary, linewidth = 0.6) +
  geom_hline(yintercept = 0, linetype = "dashed", 
             color = qqq_pal$primary, linewidth = 0.7) +
  annotate("label", 
           x = as.Date("2023-06-01"), 
           y = max(df_diff$Valor) * 0.85,
           label = paste0("Media ≈ ", round(mean(df_diff$Valor), 3)),
           fill = qqq_pal$primary, color = "white",
           fontface = "bold", size = 3.5, label.padding = unit(0.4, "lines")) +
  scale_x_date(date_breaks = "4 months", date_labels = "%b %Y",
               expand = expansion(mult = c(0.02, 0.03))) +
  scale_y_continuous(labels = scales::dollar_format(prefix = "$"),
                     expand = expansion(mult = c(0.05, 0.08))) +
  labs(
    title = "Serie Diferenciada de Primer Orden (d = 1)",
    subtitle = "QQQ: Cambios diarios en precio de cierre | Datos de entrenamiento",
    x = NULL,
    y = "Cambio Diario (USD)",
    caption = paste0("Observaciones: ", nrow(df_diff), 
                     " | Período: ", min(df_diff$Fecha), " a ", max(df_diff$Fecha))
  ) +
  theme_QQQ() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

La serie diferenciada oscila alrededor de media aproximadamente cero (0.444), sin tendencia visual evidente. La volatilidad varía a lo largo del período—períodos de calma alternando con volatilidad elevada—pero la media permanece relativamente constante, característica fundamental de estacionariedad (Hamilton, 1994).

4.2.3 Verificación Post-Diferenciación

La validación de estacionariedad post-diferenciación combina análisis ACF y test ADF:

acf_diff_data <- acf(dif_Entrenamiento, lag.max = 30, plot = FALSE)

df_acf_diff <- data.frame(
  Lag = acf_diff_data$lag[-1],
  ACF = acf_diff_data$acf[-1]
)

n_diff <- length(dif_Entrenamiento)
limite_sup_diff <- qnorm(0.975) / sqrt(n_diff)
limite_inf_diff <- -limite_sup_diff

ggplot(df_acf_diff, aes(x = Lag, y = ACF)) +
  geom_segment(aes(xend = Lag, yend = 0), 
               color = qqq_pal$secondary, linewidth = 0.8) +
  geom_point(color = qqq_pal$secondary, size = 2) +
  geom_hline(yintercept = limite_sup_diff, linetype = "dashed", 
             color = qqq_pal$primary, linewidth = 0.7) +
  geom_hline(yintercept = limite_inf_diff, linetype = "dashed", 
             color = qqq_pal$primary, linewidth = 0.7) +
  geom_hline(yintercept = 0, color = qqq_pal$text_gray, linewidth = 0.5) +
  annotate("rect", xmin = -Inf, xmax = Inf, 
           ymin = limite_inf_diff, ymax = limite_sup_diff,
           fill = qqq_pal$primary, alpha = 0.1) +
  annotate("label", x = 22, y = 0.12,
           label = "Autocorrelaciones dentro\nde bandas → Estacionaria ✓",
           fill = qqq_pal$positive, color = "white",
           fontface = "bold", size = 3.5, label.padding = unit(0.5, "lines")) +
  scale_x_continuous(breaks = seq(0, 30, 5)) +
  scale_y_continuous(limits = c(-0.15, 0.2), breaks = seq(-0.1, 0.2, 0.05)) +
  labs(
    title = "Función de Autocorrelación (ACF) - Serie Diferenciada",
    subtitle = "QQQ: Cambios diarios | Verificación de estacionariedad post-diferenciación",
    x = "Rezago (Lag)",
    y = "Autocorrelación",
    caption = "Bandas verdes: Límites de significancia al 95%"
  ) +
  theme_QQQ()

La mayoría de autocorrelaciones caen dentro de las bandas de confianza (región verde). Solo algunos rezagos (principalmente lag 1, 18, 20) muestran significancia marginal, pero el patrón general indica ausencia de raíz unitaria. Este contraste con el \(ACF\) de la serie original es dramático y valida la diferenciación.

adf_diff_resultado <- adf.test(dif_Entrenamiento)

tabla_adf_diff <- data.frame(
  Métrica = c("Estadístico Dickey-Fuller", 
              "Orden de Rezagos (Lag)", 
              "P-valor",
              "Nivel de Significancia (α)",
              "Hipótesis Nula (H₀)",
              "Decisión"),
  Valor = c(round(adf_diff_resultado$statistic, 4),
            adf_diff_resultado$parameter,
            round(adf_diff_resultado$p.value, 4),
            "0.05",
            "Serie tiene raíz unitaria",
            ifelse(adf_diff_resultado$p.value < 0.05, 
                   "Rechazar H₀", "No rechazar H₀")),
  Interpretación = c("Valor del estadístico de prueba",
                     "Rezagos incluidos en el test",
                     "Probabilidad bajo H₀",
                     "Umbral de decisión",
                     "La serie NO es estacionaria",
                     ifelse(adf_diff_resultado$p.value < 0.05,
                            "Serie ES estacionaria ✓",
                            "Serie NO estacionaria"))
)

kable(tabla_adf_diff, 
      caption = "Prueba de Dickey-Fuller Aumentada (ADF) - Serie Diferenciada (d=1)",
      align = c("l", "c", "l")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  row_spec(3, bold = TRUE, color = qqq_pal$positive, background = "#e8f5e9") %>%
  row_spec(6, bold = TRUE, background = "#d4edda", color = qqq_pal$text_dark)
Prueba de Dickey-Fuller Aumentada (ADF) - Serie Diferenciada (d=1)
Métrica Valor Interpretación
Estadístico Dickey-Fuller -8.6831 Valor del estadístico de prueba
Orden de Rezagos (Lag) 9 Rezagos incluidos en el test
P-valor 0.01 Probabilidad bajo H₀
Nivel de Significancia (α) 0.05 Umbral de decisión
Hipótesis Nula (H₀) Serie tiene raíz unitaria La serie NO es estacionaria
Decisión Rechazar H₀ Serie ES estacionaria ✓

El estadístico ADF de -8.6831 es altamente negativo, muy inferior al valor crítico de -3.43. Con p-valor de 0.01 < 0.05, se rechaza \(H_0\) con confianza . La serie diferenciada es estacionaria (Dickey & Fuller, 1979). Por tanto, el parámetro de integración en ARIMA es \(d=1\): una única diferenciación convierte la serie no-estacionaria de precios en una serie estacionaria de cambios diarios, cumpliendo con el supuesto fundamental requerido por la metodología Box-Jenkins (Box & Jenkins, 1976).


4.3 Identificación del Modelo

4.3.1 Análisis ACF/PACF - Serie Diferenciada

acf_data <- acf(dif_Entrenamiento, lag.max = 28, plot = FALSE)
pacf_data <- pacf(dif_Entrenamiento, lag.max = 28, plot = FALSE)

df_acf <- data.frame(
  Lag = as.numeric(acf_data$lag[-1]),
  Valor = as.numeric(acf_data$acf[-1])
)

df_pacf <- data.frame(
  Lag = as.numeric(pacf_data$lag),
  Valor = as.numeric(pacf_data$acf)
)

n <- length(dif_Entrenamiento)
limite <- qnorm(0.975) / sqrt(n)

p_acf <- ggplot(df_acf, aes(x = Lag, y = Valor)) +
  geom_hline(yintercept = 0, color = qqq_pal$text_gray, linewidth = 0.5) +
  geom_hline(yintercept = c(-limite, limite), linetype = "dashed", 
             color = qqq_pal$secondary, linewidth = 0.6) +
  annotate("rect", xmin = -Inf, xmax = Inf, ymin = -limite, ymax = limite,
           fill = qqq_pal$secondary, alpha = 0.08) +
  geom_segment(aes(xend = Lag, yend = 0), color = qqq_pal$primary, linewidth = 0.7) +
  geom_point(color = qqq_pal$primary, size = 1.5) +
  scale_x_continuous(breaks = seq(0, 28, 5)) +
  scale_y_continuous(limits = c(-0.15, 0.12)) +
  labs(title = "ACF - Serie Diferenciada",
       subtitle = "Identificación del orden q (MA)",
       x = "Rezago (Lag)",
       y = "ACF") +
  theme_QQQ() +
  theme(plot.title = element_text(size = 12))

p_pacf <- ggplot(df_pacf, aes(x = Lag, y = Valor)) +
  geom_hline(yintercept = 0, color = qqq_pal$text_gray, linewidth = 0.5) +
  geom_hline(yintercept = c(-limite, limite), linetype = "dashed", 
             color = qqq_pal$secondary, linewidth = 0.6) +
  annotate("rect", xmin = -Inf, xmax = Inf, ymin = -limite, ymax = limite,
           fill = qqq_pal$secondary, alpha = 0.08) +
  geom_segment(aes(xend = Lag, yend = 0), color = qqq_pal$primary, linewidth = 0.7) +
  geom_point(color = qqq_pal$primary, size = 1.5) +
  scale_x_continuous(breaks = seq(0, 28, 5)) +
  scale_y_continuous(limits = c(-0.15, 0.12)) +
  labs(title = "PACF - Serie Diferenciada",
       subtitle = "Identificación del orden p (AR)",
       x = "Rezago (Lag)",
       y = "PACF") +
  theme_QQQ() +
  theme(plot.title = element_text(size = 12))

grid.arrange(p_acf, p_pacf, ncol = 2)

El análisis visual del \(ACF\) y \(PACF\) de la serie diferenciada del \(QQQ\) revela patrones que son característicos de mercados financieros eficientes. En el \(ACF\) (panel izquierdo), se observa que la mayoría de autocorrelaciones caen dentro de las bandas de confianza teóricas para todos los rezagos hasta lag 28. Esta ausencia sistemática de autocorrelaciones significativas se interpreta según la hipótesis de mercado eficiente (EMH): los precios de activos seguros incorporan información disponible públicamente, dejando en los retornos una estructura prácticamente aleatoria sin dependencias predecibles (Fama, 1970; Malkiel, 1973). La presencia de algunas autocorrelaciones leves (por ejemplo, alrededor de lag 17-20) no alcanza magnitud para ser interpretada como estructura genuina sino más bien como ruido muestral esperado en series finitas.

El \(PACF\) (panel derecho) exhibe un patrón complementario: nuevamente, la mayoría de autocorrelaciones parciales se ubican dentro de bandas de confianza, con excepción de quizás lag 1 que muestra una pequeña autocorrelación parcial negativa. En conjunto, el análisis \(ACF/PACF\) sugiere que la serie diferenciada es cercana a ruido blanco, es decir, una secuencia de innovaciones aleatorias sin estructura de dependencia predecible. Este hallazgo tiene implicaciones profundas: implica que cualquier modelo \(ARIMA\) que intente capturar estructura en precios del \(QQQ\) estará fundamentalmente limitado en su capacidad predictiva, capturando quizás un componente pequeño de ineficiencia de mercado de corto plazo más que una estructura genuina de largo plazo (Tsay, 2010; Hamilton, 1994).

4.3.2 Modelos ARIMA Candidatos

Dada la evidencia de escasa estructura predecible en los correlogramas, la identificación manual procede evaluando un conjunto de modelos candidatos que van desde el más simple (ARIMA(0,1,0), random walk puro) hasta especificaciones progresivamente más complejas. La estrategia de selección combina tres criterios complementarios: (i) plausibilidad teórica basada en observación de \(ACF/PACF\), (ii) parsimonia (principio de máxima simplicidad), y (iii) comparación formal mediante criterios de información que balancean bondad de ajuste contra número de parámetros (Hyndman & Athanasopoulos, 2021).

tabla_candidatos <- data.frame(
  Modelo = c("ARIMA(0,1,0)", 
             "ARIMA(1,1,1)",
             "ARIMA(2,1,1)", 
             "ARIMA(1,1,2)",
             "ARIMA(2,1,2)",
             "ARIMA(3,1,3)"),
  Tipo = c("Random Walk",
           "auto.arima()",
           "Manual",
           "Manual",
           "Manual",
           "Exploratorio"),
  `Observación ACF/PACF` = c(
    "Patrón general cercano a ruido blanco",
    "Selección automática por AICc",
    "Posible estructura en lags 1-2 del PACF",
    "Posible estructura en lags 1-2 del ACF",
    "Combinación de estructuras en ambos correlogramas",
    "Pico marginal en lag 3 de ambos correlogramas"
  ),
  Justificación = c(
    "Benchmark obligatorio: hipótesis de mercado eficiente",
    "Referencia algorítmica para validar selección manual",
    "Extensión AR(2) para capturar persistencia de corto plazo",
    "Extensión MA(2) para capturar estructura de media móvil",
    "Modelo simétrico que combina dinámicas AR y MA",
    "Evaluar si rezagos marginales aportan capacidad predictiva"
  )
)

kable(tabla_candidatos,
      caption = "Modelos ARIMA Candidatos para Evaluación",
      align = c("l", "c", "l", "l"),
      col.names = c("Modelo", "Tipo", "Observación en ACF/PACF", "Justificación")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, color = qqq_pal$primary) %>%
  column_spec(2, color = qqq_pal$secondary) %>%
  column_spec(3, width = "18em") %>%
  column_spec(4, width = "22em") %>%
  row_spec(2, background = "#e8f5e9", color = qqq_pal$text_dark, bold = TRUE)
Modelos ARIMA Candidatos para Evaluación
Modelo Tipo Observación en ACF/PACF Justificación
ARIMA(0,1,0) Random Walk Patrón general cercano a ruido blanco Benchmark obligatorio: hipótesis de mercado eficiente
ARIMA(1,1,1) auto.arima() Selección automática por AICc Referencia algorítmica para validar selección manual
ARIMA(2,1,1) Manual Posible estructura en lags 1-2 del PACF Extensión AR(2) para capturar persistencia de corto plazo
ARIMA(1,1,2) Manual Posible estructura en lags 1-2 del ACF Extensión MA(2) para capturar estructura de media móvil
ARIMA(2,1,2) Manual Combinación de estructuras en ambos correlogramas Modelo simétrico que combina dinámicas AR y MA
ARIMA(3,1,3) Exploratorio Pico marginal en lag 3 de ambos correlogramas Evaluar si rezagos marginales aportan capacidad predictiva

La selección de estos seis modelos candidatos obedece a una lógica progresiva de complejidad. El \(ARIMA(0,1,0)\)—conocido como random walk en finanzas—representa la hipótesis nula de mercado eficiente: no existe estructura explorable, y el mejor pronóstico del precio mañana es simplemente el precio hoy más una innovación aleatoria. Este modelo actúa como benchmark obligatorio contra el cual se evalúan modelos más sofisticados (Fama, 1970; Malkiel, 1973). El \(ARIMA(1,1,1)\) fue seleccionado por la función auto.arima() que utiliza criterios de información (Akaike, BIC) para seleccionar automáticamente órdenes. Este modelo es el más parsimonioso que añade estructura genuina: un componente autorregresivo captura dependencia del precio rezagado un período, mientras que un componente media móvil captura el efecto de innovaciones rezagadas. La función de verosimilitud bajo \(ARIMA(1,1,1)\) captura un componente adicional de dependencia que no existe en el random walk puro, aunque sea pequeño (Tsay, 2010). El \(ARIMA(2,1,1)\) extiende el componente \(AR\) a dos rezagos para evaluar si la persistencia de corto plazo requiere más de un rezago histórico. El \(ARIMA(1,1,2)\) extiende en cambio el componente \(MA\), explorando si el efecto de innovaciones requiere dos períodos de memoria. El \(ARIMA(2,1,2)\) representa un modelo simétrico que combina ambas dinámicas. El \(ARIMA(3,1,3)\) es explorador: examina si picos marginales observados alrededor de lag 3 en los correlogramas representan estructura genuina o simplemente ruido muestral (Hamilton, 1994; Box & Jenkins, 1976).


4.4 Estimación y Comparación de Modelos

4.4.1 Criterios de Información

ModeloQA <- auto.arima(Entrenamiento)
modeloQ1 <- Arima(Entrenamiento, order = c(3,1,3))
modeloQ2 <- Arima(Entrenamiento, order = c(0,1,0))
modeloQ3 <- Arima(Entrenamiento, order = c(2,1,1))
modeloQ4 <- Arima(Entrenamiento, order = c(1,1,2))
modeloQ5 <- Arima(Entrenamiento, order = c(2,1,2))
comparacion_IC <- data.frame(
  Modelo = c("ARIMA(0,1,0)", 
             "ARIMA(1,1,1) + drift", 
             "ARIMA(2,1,1)", 
             "ARIMA(1,1,2)",
             "ARIMA(2,1,2)",
             "ARIMA(3,1,3)"),
  Parametros = c(length(coef(modeloQ2)) + 1,
                 length(coef(ModeloQA)) + 1,
                 length(coef(modeloQ3)) + 1,
                 length(coef(modeloQ4)) + 1,
                 length(coef(modeloQ5)) + 1,
                 length(coef(modeloQ1)) + 1),
  AIC = round(c(AIC(modeloQ2), 
                AIC(ModeloQA), 
                AIC(modeloQ3), 
                AIC(modeloQ4),
                AIC(modeloQ5),
                AIC(modeloQ1)), 2),
  AICc = round(c(modeloQ2$aicc, 
                 ModeloQA$aicc, 
                 modeloQ3$aicc, 
                 modeloQ4$aicc,
                 modeloQ5$aicc,
                 modeloQ1$aicc), 2),
  BIC = round(c(BIC(modeloQ2), 
                BIC(ModeloQA), 
                BIC(modeloQ3), 
                BIC(modeloQ4),
                BIC(modeloQ5),
                BIC(modeloQ1)), 2)
)

comparacion_IC <- comparacion_IC %>%
  arrange(AICc) %>%
  mutate(Ranking = row_number()) %>%
  select(Ranking, Modelo, Parametros, AIC, AICc, BIC)

kable(comparacion_IC,
      caption = "Comparación de Modelos por Criterios de Información",
      align = c("c", "l", "c", "c", "c", "c"),
      col.names = c("Ranking", "Modelo", "# Parámetros", "AIC", "AICc", "BIC")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  column_spec(2, bold = TRUE, color = qqq_pal$primary) %>%
  column_spec(5, bold = TRUE, color = qqq_pal$positive) %>%
  row_spec(1, bold = TRUE, background = "#e8f5e9", color = qqq_pal$text_dark) %>%
  footnote(general = "Ordenado por AICc (menor es mejor). AICc es el criterio preferido para muestras finitas.",
           general_title = "Nota: ")
Comparación de Modelos por Criterios de Información
Ranking Modelo # Parámetros AIC AICc BIC
1 ARIMA(1,1,1) + drift 4 4663.89 4663.94 4682.35
2 ARIMA(2,1,2) 5 4668.06 4668.14 4691.13
3 ARIMA(0,1,0) 1 4668.38 4668.39 4673.00
4 ARIMA(1,1,2) 4 4668.46 4668.52 4686.92
5 ARIMA(2,1,1) 4 4668.52 4668.58 4686.98
6 ARIMA(3,1,3) 7 4669.04 4669.19 4701.34
Nota:
Ordenado por AICc (menor es mejor). AICc es el criterio preferido para muestras finitas.

En el análisis, el \(ARIMA(1,1,1)\) + drift emerge como ganador con \(AICc\) = 4663.94, significativamente menor que \(ARIMA(2,1,2)\) (4668.14, ranking 2) y todos los demás modelos. Esta diferencia de 4.2 puntos en \(AICc\) entre ranking 1 y 2 es sustancial: según regla de oro de Burnham & Anderson (2002), diferencias >10 en \(AICc\) indican que modelo inferior tiene esencialmente probabilidad posterior negligible; diferencias entre 4-7 indican soporte moderado para modelo superior. La diferencia de 4.2 en nuestro caso es clara aunque no dramática, validando \(ARIMA(1,1,1)\) como notablemente superior.

El término “drift” en \(ARIMA(1,1,1)\) + drift representa una constante en la ecuación de diferencias: \(∇y_t = μ + φ₁∇y_{t-1} + θ₁ε_{t-1} + ε_t\), donde μ captura tendencia lineal implícita en la serie. En economía financiera, el drift representa el retorno esperado promedio (equity premium): durante el período de análisis (2022-2025), el \(QQQ\) exhibió movimiento alcista promedio (drift positivo), reflejando optimismo en tecnología pese a volatilidad. El algoritmo auto.arima() seleccionó automáticamente este drift porque mejoró sustancialmente el criterio \(AICc\) (Brockwell & Davis, 2016).

La evaluación de modelos más complejos (ARIMA(3,1,3) con 7 parámetros) revela el problema de sobreparametrización: aunque tiene \(AIC\) más bajo (4669.04), su \(AICc\) es 4669.19 (ranking 6) porque la penalización por 7 parámetros domina cualquier mejora marginal en ajuste. Este es un caso de castigo de parsimonia: modelo más complejo no justifica su complejidad. El \(ARIMA(0,1,0)\) (random walk puro), por el contrario, es más parsimonioso (1 parámetro) pero produce \(AICc =\) 4668.39 (ranking 3), confirmando lo que los correlogramas sugirieron: estructura existe aunque sea pequeña, y modelo más complejo la captura mejor que modelo más simple.

4.4.2 Métricas de Precisión en Entrenamiento

acc_QA <- accuracy(ModeloQA)
acc_Q1 <- accuracy(modeloQ1)
acc_Q2 <- accuracy(modeloQ2)
acc_Q3 <- accuracy(modeloQ3)
acc_Q4 <- accuracy(modeloQ4)
acc_Q5 <- accuracy(modeloQ5)

comparacion_accuracy <- data.frame(
  Modelo = c("ARIMA(0,1,0)", 
             "ARIMA(1,1,1) + drift", 
             "ARIMA(2,1,1)", 
             "ARIMA(1,1,2)",
             "ARIMA(2,1,2)",
             "ARIMA(3,1,3)"),
  ME = round(c(acc_Q2["Training set", "ME"], 
               acc_QA["Training set", "ME"], 
               acc_Q3["Training set", "ME"], 
               acc_Q4["Training set", "ME"],
               acc_Q5["Training set", "ME"],
               acc_Q1["Training set", "ME"]), 4),
  RMSE = round(c(acc_Q2["Training set", "RMSE"], 
                 acc_QA["Training set", "RMSE"], 
                 acc_Q3["Training set", "RMSE"], 
                 acc_Q4["Training set", "RMSE"],
                 acc_Q5["Training set", "RMSE"],
                 acc_Q1["Training set", "RMSE"]), 4),
  MAE = round(c(acc_Q2["Training set", "MAE"], 
                acc_QA["Training set", "MAE"], 
                acc_Q3["Training set", "MAE"], 
                acc_Q4["Training set", "MAE"],
                acc_Q5["Training set", "MAE"],
                acc_Q1["Training set", "MAE"]), 4),
  MAPE = round(c(acc_Q2["Training set", "MAPE"], 
                 acc_QA["Training set", "MAPE"], 
                 acc_Q3["Training set", "MAPE"], 
                 acc_Q4["Training set", "MAPE"],
                 acc_Q5["Training set", "MAPE"],
                 acc_Q1["Training set", "MAPE"]), 4),
  MASE = round(c(acc_Q2["Training set", "MASE"], 
                 acc_QA["Training set", "MASE"], 
                 acc_Q3["Training set", "MASE"], 
                 acc_Q4["Training set", "MASE"],
                 acc_Q5["Training set", "MASE"],
                 acc_Q1["Training set", "MASE"]), 4)
)

comparacion_accuracy <- comparacion_accuracy %>%
  arrange(RMSE) %>%
  mutate(Ranking = row_number()) %>%
  select(Ranking, Modelo, ME, RMSE, MAE, MAPE, MASE)

kable(comparacion_accuracy,
      caption = "Métricas de Precisión sobre Datos de Entrenamiento",
      align = c("c", "l", rep("c", 5))) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  column_spec(2, bold = TRUE, color = qqq_pal$primary) %>%
  column_spec(4, bold = TRUE, color = qqq_pal$positive) %>%
  row_spec(1, background = "#e8f5e9", color = qqq_pal$text_dark, bold = TRUE) %>%
  footnote(general = "ME: Error Medio | RMSE: Raíz del Error Cuadrático Medio | MAE: Error Absoluto Medio | MAPE: Error Porcentual (%) | MASE: Error Escalado",
           general_title = "Métricas: ")
Métricas de Precisión sobre Datos de Entrenamiento
Ranking Modelo ME RMSE MAE MAPE MASE
1 ARIMA(3,1,3) 0.4939 5.4759 3.9216 0.9540 1.0074
2 ARIMA(1,1,1) + drift -0.0001 5.4792 3.8704 0.9445 0.9943
3 ARIMA(2,1,2) 0.4405 5.4867 3.8894 0.9449 0.9992
4 ARIMA(1,1,2) 0.4439 5.4960 3.9003 0.9500 1.0020
5 ARIMA(2,1,1) 0.4446 5.4962 3.9000 0.9499 1.0019
6 ARIMA(0,1,0) 0.4438 5.5179 3.8878 0.9444 0.9988
Métricas:
ME: Error Medio | RMSE: Raíz del Error Cuadrático Medio | MAE: Error Absoluto Medio | MAPE: Error Porcentual (%) | MASE: Error Escalado

El Error Medio (ME) de todos los modelos es cercano a cero (rango: -0.0001 a 0.4939), confirmando que son aproximadamente insesgados. El \(ARIMA(1,1,1)\) + drift tiene \(ME =\) -0.0001, virtualmente perfecto. El \(RMSE\) del \(ARIMA(3,1,3)\) lidera con 5.4759, marginalmente inferior al \(ARIMA(1,1,1)\) + drift (5.4792), una diferencia de 0.0033 negligible en términos prácticos (0.06% mejora) mientras requiere 75% incremento en parámetros.

El Error Absoluto Medio (MAE) del \(ARIMA(1,1,1)\) + drift es 3.8704: en promedio, predicciones desviaron ±3.87 puntos del valor real. El MAPE es 0.9445%, indicando error promedio menos de 1% del valor real. El MASE de todos los modelos es < 1, mejorando al random walk puro. El \(ARIMA(1,1,1)\) + drift obtiene \(MASE =\) 0.9943, mejorando al random walk en ~0.6%—en contexto de series financieras eficientes, una mejora significativa (Hyndman & Koehler, 2006).

La síntesis de criterios de información y métricas de precisión conduce a una conclusión robusta: \(ARIMA(1,1,1)\) con drift es el modelo seleccionado. Aunque \(ARIMA(3,1,3)\) tiene \(RMSE\) ligeramente menor, la ventaja es marginal (0.0033 puntos) mientras que requiere 75% incremento en parámetros, violando principio de parsimonia. \(AICc\) de \(ARIMA(1,1,1)\) + drift (4663.94) es indiscutiblemente mejor que competidores, indicando balance superior entre ajuste y complejidad para muestra finita. Desde perspectiva práctica, el modelo captura estructura de drift (tendencia alcista genuina en \(QQQ\) durante período) mediante un parámetro adicional, permitiendo que pronósticos incorporen esta tendencia en lugar de asumir movimiento aleatorio puro (Box & Jenkins, 1976; Brockwell & Davis, 2016).


4.5 Diagnóstico de Residuos

El proceso de diagnóstico valida que el modelo \(ARIMA(1,1,1)\) + drift seleccionado produce residuos que se comportan como ruido blanco: secuencia aleatoria con media cero, varianza constante, y ausencia de autocorrelación.

4.5.1 Análisis Gráfico de Residuos

residuos <- residuals(ModeloQA)

df_residuos <- data.frame(
  Fecha = index(residuos),
  Residuo = as.numeric(residuos)
)


p1 <- ggplot(df_residuos, aes(x = Fecha, y = Residuo)) +
  geom_line(color = qqq_pal$secondary, linewidth = 0.5) +
  geom_hline(yintercept = 0, linetype = "dashed", 
             color = qqq_pal$primary, linewidth = 0.7) +
  geom_hline(yintercept = c(-2*sd(df_residuos$Residuo), 2*sd(df_residuos$Residuo)), 
             linetype = "dotted", color = qqq_pal$negative, linewidth = 0.5) +
  scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
  labs(title = "Residuos del Modelo en el Tiempo",
       subtitle = "Verificación de media cero y varianza constante",
       x = NULL,
       y = "Residuo") +
  theme_QQQ() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

p1

acf_resid <- acf(residuos, lag.max = 25, plot = FALSE)
df_acf_resid <- data.frame(
  Lag = as.numeric(acf_resid$lag[-1]),
  ACF = as.numeric(acf_resid$acf[-1])
)

n_resid <- length(residuos)
limite_resid <- qnorm(0.975) / sqrt(n_resid)

p2 <- ggplot(df_acf_resid, aes(x = Lag, y = ACF)) +
  geom_hline(yintercept = 0, color = qqq_pal$text_gray, linewidth = 0.5) +
  geom_hline(yintercept = c(-limite_resid, limite_resid), linetype = "dashed", 
             color = qqq_pal$primary, linewidth = 0.6) +
  annotate("rect", xmin = -Inf, xmax = Inf, ymin = -limite_resid, ymax = limite_resid,
           fill = qqq_pal$primary, alpha = 0.1) +
  geom_segment(aes(xend = Lag, yend = 0), color = qqq_pal$secondary, linewidth = 0.7) +
  geom_point(color = qqq_pal$secondary, size = 1.5) +
  scale_x_continuous(breaks = seq(0, 25, 5)) +
  scale_y_continuous(limits = c(-0.15, 0.15)) +
  labs(title = "ACF de Residuos",
       subtitle = "Verificación de independencia",
       x = "Rezago (Lag)",
       y = "ACF") +
  theme_QQQ()
p2

p3 <- ggplot(df_residuos, aes(x = Residuo)) +
  geom_histogram(aes(y = after_stat(density)), 
                 bins = 35, fill = qqq_pal$primary, 
                 color = "white", alpha = 0.7) +
  geom_density(color = qqq_pal$secondary, linewidth = 1) +
  stat_function(fun = dnorm, 
                args = list(mean = mean(df_residuos$Residuo), 
                            sd = sd(df_residuos$Residuo)),
                color = qqq_pal$negative, linewidth = 1, linetype = "dashed") +
  labs(title = "Distribución de Residuos",
       subtitle = "Verificación de normalidad",
       x = "Residuo",
       y = "Densidad",
       caption = "Línea roja punteada: distribución normal teórica") +
  theme_QQQ()
p3

El primer componente del diagnóstico examina la comportamiento temporal de los residuos (panel superior). El gráfico exhibe oscilación de residuos alrededor de una media próxima a cero, con bandas de confianza aproximadas ubicadas a ±2 desviaciones estándar. La media aritmética de los residuos es prácticamente cero (\(E[εₜ] ≈\) 0.0001), validando que el modelo es aproximadamente insesgado: no subestima ni sobrestima sistemáticamente. La varianza aparece aproximadamente constante a lo largo del período completo (octubre 2022 - septiembre 2025), sugiriendo homocedasticidad: la volatilidad de errores no cambia dramáticamente. Existe, sin embargo, un período de mayor volatilidad alrededor de noviembre 2024 donde se observa un residuo extremo (~+50), que coincide con movimientos de volatilidad elevada en mercados tecnológicos durante esa fecha.

El segundo componente examina la función de autocorrelación de residuos (panel central). En teoría, si los residuos constituyen ruido blanco verdadero, el \(ACF\) debe mostrar prácticamente todas las autocorrelaciones dentro de las bandas de confianza de. Este caso exhibe esta característica: la mayoría de las 25 autocorrelaciones evaluadas caen dentro de bandas (líneas de guiones azules). Se observan algunos picos leves fuera de bandas, pero estos son aislados y de magnitud pequeña. Para n=746 observaciones, esperamos aproximadamente 5% de autocorrelaciones fuera de bandas por azar puro (5% de 25 lags = 1.25 lags esperados). Observar ~2-3 lags fuera de bandas es consistente con ruido aleatorio, sin indicación de autocorrelación residual sistemática (Box & Jenkins, 1976; Hyndman & Athanasopoulos, 2021).

El tercer componente examina la distribución de residuos (panel inferior). El histograma de densidad (barras verde) muestra forma aproximadamente simétrica y unimodal, consistente con una distribución normal. La curva de densidad empírica (línea azul) se superpone adecuadamente con la curva de distribución normal teórica (línea roja punteada) en la región central. Sin embargo, se observa que las colas de la distribución empírica son ligeramente más pesadas que lo predicho por la teoría normal: el histograma muestra más observaciones en regiones extremas (residuos < -15 y > +30) que una normal pura. Este fenómeno de “fat tails” (colas gordas) es característico de datos financieros reales, donde eventos extremos ocurren con probabilidad ligeramente mayor que lo predicho por distribución normal (Tsay, 2010; Hamilton, 1994). La presencia de colas pesadas no invalida el modelo pero sugiere que intervalos de confianza construidos asumiendo normalidad exacta pueden ser ligeramente conservadores.

4.5.2 Q-Q Plot de Normalidad

residuos_std <- scale(residuals(ModeloQA))
df_qq <- data.frame(residuos = residuos_std)

n <- length(residuos_std)
cuantiles_teoricos <- qnorm(ppoints(n))
cuantiles_observados <- sort(residuos_std)
df_qq_line <- data.frame(x = cuantiles_teoricos, y = cuantiles_observados)

fit <- lm(y ~ x, data = df_qq_line)
df_qq_line$fitted <- predict(fit, df_qq_line)

df_qq_puntos <- data.frame(
  x = cuantiles_teoricos,
  y = cuantiles_observados,
  label = paste0(
    "Cuantil teórico: ", round(cuantiles_teoricos, 3), "<br>",
    "Residuo observado: ", round(cuantiles_observados, 3)
  )
)

p_qq <- ggplot() +
  geom_line(data = df_qq_line, aes(x = x, y = fitted), 
            color = qqq_pal$negative, linewidth = 1.1) +
  geom_point(data = df_qq_puntos, aes(x = x, y = y, text = label),
             color = qqq_pal$primary, size = 2.5, alpha = 0.75) +
  labs(
    title = "Q-Q Plot de Residuos Estandarizados",
    subtitle = "Verificación de normalidad del modelo | Línea roja = distribución normal teórica",
    x = "Cuantiles Teóricos (Distribución Normal Estándar)",
    y = "Cuantiles Observados (Residuos Estandarizados)",
    caption = "✓ Puntos alineados con la línea roja indican buenos residuos normales"
  ) +
  theme_QQQ() +
  theme(
    plot.title = element_text(size = 13, face = "bold", color = qqq_pal$primary),
    plot.subtitle = element_text(size = 11, color = qqq_pal$text_gray, margin = margin(b = 8)),
    plot.caption = element_text(size = 9, color = qqq_pal$secondary, face = "italic"),
    panel.background = element_rect(fill = "#f8f9fa", color = NA),
    plot.background = element_rect(fill = "white", color = NA),
    axis.line = element_line(color = qqq_pal$text_gray, linewidth = 0.5),
    panel.grid.major = element_line(color = "#e8eef5", linewidth = 0.3),
    panel.grid.minor = element_blank()
  )

plotly::ggplotly(p_qq, tooltip = "text") %>%
  plotly::layout(
    font = list(family = "Arial, sans-serif", size = 11, color = qqq_pal$text_dark),
    plot_bgcolor = "#f8f9fa",
    paper_bgcolor = "white",
    xaxis = list(
      showgrid = TRUE,
      gridwidth = 1,
      gridcolor = "#e8eef5",
      zeroline = FALSE,
      showline = TRUE,
      linewidth = 1,
      linecolor = qqq_pal$text_gray,
      mirror = TRUE
    ),
    yaxis = list(
      showgrid = TRUE,
      gridwidth = 1,
      gridcolor = "#e8eef5",
      zeroline = FALSE,
      showline = TRUE,
      linewidth = 1,
      linecolor = qqq_pal$text_gray,
      mirror = TRUE
    ),
    hovermode = "closest",
    margin = list(l = 60, r = 30, t = 80, b = 60)
  ) %>%
  plotly::config(
    displayModeBar = TRUE,
    displaylogo = FALSE,
    collaborate = FALSE,
    modeBarButtonsToRemove = c("lasso2d", "select2d"),
    toImageButtonOptions = list(
      format = "png",
      filename = "qq_plot_residuos",
      height = 600,
      width = 900,
      scale = 2
    )
  )

En el \(Q-Q\) \(plot\), se observa alineación muy cercana de puntos con la línea teórica en la región central (cuantiles entre -2 y +2, que representan aproximadamente 95% de la distribución). Esta es la característica más importante: en esta región central donde residen la mayoría de observaciones, la normalidad se cumple aproximadamente. Sin embargo, en las colas (cuantiles teóricos < -2 o > +2), se observa desviación sistemática: los cuantiles observados en la cola superior se sitúan por encima de la línea teórica, indicando que residuos extremos positivos son más extremos que lo predicho por una normal. Esto confirma la presencia de “fat tails” ya sugerida por el histograma.

Esta característica de colas pesadas es típica de retornos de activos financieros y no invalida el modelo \(ARIMA\), pero tiene implicaciones para inferencia: intervalos de confianza construidos asumiendo normalidad exacta pueden subesticar probabilidades de eventos extremos, potencialmente subestimando riesgo de cola (tail risk) en pronósticos. Sin embargo, para propósitos de pronóstico de series de tiempo diarias en contexto de análisis académico, la aproximación normal en la región central es suficiente (Tsay, 2010; Brockwell & Davis, 2016).

4.5.3 Test de Independencia (Ljung-Box)

lb_test <- Box.test(residuals(ModeloQA), lag = 10, type = "Ljung-Box")

tabla_ljung <- data.frame(
  Métrica = c("Estadístico Ljung-Box", 
              "Grados de Libertad", 
              "P-valor",
              "Conclusión"),
  Valor = c(
    round(lb_test$statistic, 4),
    lb_test$parameter,
    round(lb_test$p.value, 4),
    ifelse(lb_test$p.value > 0.05, 
           "Residuos son ruido blanco ✓", 
           "Posible autocorrelación residual")
  )
)

kable(tabla_ljung, 
      caption = "Test de Ljung-Box: Independencia de Residuos",
      align = c("l", "c")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  row_spec(4, bold = TRUE, 
           background = ifelse(lb_test$p.value > 0.05, "#e8f5e9", "#ffe8e0"),
           color = qqq_pal$text_dark)
Test de Ljung-Box: Independencia de Residuos
Métrica Valor
Estadístico Ljung-Box 10.1399
Grados de Libertad 10
P-valor 0.4283
Conclusión Residuos son ruido blanco ✓

El estadístico Ljung-Box de \(Q^*\) = 10.1399 con 10 grados de libertad produce p-valor = 0.4283 > 0.05. No se rechaza la hipótesis nula: los residuos no exhiben autocorrelación significativa en los primeros 10 rezagos. El \(p-valor\) de 0.43 indica que si verdaderamente los residuos fuesen ruido blanco independiente, observaríamos una estadística de prueba tan extrema o más extrema con probabilidad del 43%—resultado completamente consistente con independencia (Ljung & Box, 1978).

El único hallazgo notable es la presencia de algunos residuos extremos (especialmente el outlier de ~+50 en noviembre 2024), que coincide con período de volatilidad elevada en mercados tecnológicos. Estos son errores de predicción grandes pero aislados, no sistemáticos, consistentes con shocks de mercado exógenos que el modelo \(ARIMA\) no puede anticipar (Tsay, 2010; Brockwell & Davis, 2016). En conclusión, el modelo \(ARIMA(1,1,1)\) + drift está correctamente especificado y procede a la etapa de pronóstico.


4.6 Pronóstico y Evaluación

Una vez validado que el modelo \(ARIMA(1,1,1)\) + drift produce residuos que se comportan como ruido blanco, procede la etapa de pronóstico: generación de predicciones puntuales para el \(QQQ\) junto con intervalos de confianza que cuantifican incertidumbre.

pronostico <- forecast(ModeloQA, h = 10, level = 95)

ultima_fecha <- as.Date(index(Entrenamiento)[length(Entrenamiento)])

fechas_pronostico <- c()
fecha_actual <- ultima_fecha
dias_agregados <- 0

while(dias_agregados < 10) {
  fecha_actual <- fecha_actual + 1
  if (!(weekdays(fecha_actual) %in% c("sábado", "domingo", "Saturday", "Sunday"))) {
    fechas_pronostico <- c(fechas_pronostico, fecha_actual)
    dias_agregados <- dias_agregados + 1
  }
}

fechas_pronostico <- as.Date(fechas_pronostico, origin = "1970-01-01")

4.6.1 Tabla de Pronósticos

tabla_pronostico <- data.frame(
  Día = 1:10,
  Fecha = as.character(fechas_pronostico),
  Pronóstico = round(as.numeric(pronostico$mean), 2),
  `Límite Inferior` = round(as.numeric(pronostico$lower), 2),
  `Límite Superior` = round(as.numeric(pronostico$upper), 2),
  `Amplitud IC` = round(as.numeric(pronostico$upper) - as.numeric(pronostico$lower), 2)
)

kable(tabla_pronostico,
      caption = "Pronósticos del Modelo ARIMA(1,1,1) con Drift - Intervalo de Confianza al 95%",
      align = c("c", "c", "c", "c", "c", "c"),
      col.names = c("Día", "Fecha", "Pronóstico (USD)", "Lím. Inferior", "Lím. Superior", "Amplitud IC")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, color = qqq_pal$primary) %>%
  column_spec(3, bold = TRUE, color = qqq_pal$primary, background = "#f0f9ff") %>%
  column_spec(4, color = qqq_pal$negative) %>%
  column_spec(5, color = qqq_pal$positive) %>%
  column_spec(6, color = "#666666") %>%
  footnote(general = "IC = Intervalo de Confianza. La amplitud del intervalo aumenta con el horizonte de pronóstico.",
           general_title = "Nota: ")
Pronósticos del Modelo ARIMA(1,1,1) con Drift - Intervalo de Confianza al 95%
Día Fecha Pronóstico (USD) Lím. Inferior Lím. Superior Amplitud IC
1 2025-10-01 600.71 589.94 611.47 21.54
2 2025-10-02 601.23 586.43 616.03 29.60
3 2025-10-03 601.61 583.40 619.83 36.42
4 2025-10-06 602.11 581.20 623.01 41.81
5 2025-10-07 602.51 579.11 625.92 46.82
6 2025-10-08 602.99 577.40 628.57 51.17
7 2025-10-09 603.41 575.76 631.06 55.30
8 2025-10-10 603.87 574.34 633.40 59.06
9 2025-10-13 604.30 572.98 635.63 62.65
10 2025-10-14 604.76 571.75 637.76 66.01
Nota:
IC = Intervalo de Confianza. La amplitud del intervalo aumenta con el horizonte de pronóstico.

Los pronósticos puntuales generados por el modelo \(ARIMA(1,1,1)\) + drift exhiben un patrón revelador: los precios predichos para los próximos 10 días hábiles (01-octubre a 14-octubre 2025) se ubican en el rango 600.71 a 604.76 USD, con trayectoria que muestra incremento sostenido pero moderado (pendiente del drift positiva pero pequeña). El primer pronóstico (día 1, 01-octubre) es 600.71, comparado con el último precio observado al 30-septiembre de aproximadamente 599.58. Este incremento inicial de ~$1.13 refleja la estimación de drift del modelo: una tendencia alcista promedio de μ ≈ 0.4-0.5 dólares por día (aproximadamente 0.07% diario o 1.4% anual), capturable mediante el término constante en la ecuación de diferencias (Brockwell & Davis, 2016; Box & Jenkins, 1976).

La característica más notable de los pronósticos es la amplitud expansiva de los intervalos de confianza: el IC 95% en el día 1 tiene amplitud de apenas 21.54 (aproximadamente ±10.77 alrededor del pronóstico puntual), pero se expande a 66.01 en el día 10 (aproximadamente ±33). Esta expansión obedece a la naturaleza de la propagación de incertidumbre en pronósticos: conforme aumenta el horizonte h, la varianza del pronóstico \(ε_h\) bajo un \(ARIMA(1,1,1)\) crece aproximadamente como \(σ²·h\) (para ARIMA no estacionario con d=1), donde \(σ²\) es la varianza de innovaciones. Así, mientras el modelo predice una trayectoria promedio suavemente alcista, reconoce que verdadero precio futuro puede desviarse de esta trayectoria de manera creciente, reflejando la característica fundamental de mercados financieros eficientes: la predictibilidad diminuye exponencialmente con horizonte (Hamilton, 1994; Malkiel, 1973).

4.6.2 Gráfico: Pronóstico con Intervalo de Confianza

n_historico <- 100
datos_hist <- tail(Entrenamiento, n_historico)

df_historico <- data.frame(
  Fecha = as.Date(index(datos_hist)),
  Precio = as.numeric(datos_hist),
  Tipo = "Histórico"
)

ultima_fecha <- as.Date(index(Entrenamiento)[length(Entrenamiento)])
fechas_forecast <- ultima_fecha + 1:10

df_pronostico <- data.frame(
  Fecha = fechas_forecast,
  Precio = as.numeric(pronostico$mean),
  Lower = as.numeric(pronostico$lower),
  Upper = as.numeric(pronostico$upper)
)

punto_conexion <- data.frame(
  Fecha = ultima_fecha,
  Precio = as.numeric(tail(datos_hist, 1)),
  Lower = as.numeric(tail(datos_hist, 1)),
  Upper = as.numeric(tail(datos_hist, 1))
)

df_pronostico_completo <- bind_rows(punto_conexion, df_pronostico)

ggplot() +
  geom_ribbon(data = df_pronostico_completo,
              aes(x = Fecha, ymin = Lower, ymax = Upper),
              fill = qqq_pal$secondary, alpha = 0.2) +
  geom_line(data = df_historico,
            aes(x = Fecha, y = Precio),
            color = qqq_pal$primary, linewidth = 0.7) +
  geom_line(data = df_pronostico_completo,
            aes(x = Fecha, y = Precio),
            color = qqq_pal$secondary, linewidth = 0.8) +
  geom_point(data = df_pronostico %>% filter(Fecha == max(Fecha)),
             aes(x = Fecha, y = Precio),
             color = qqq_pal$secondary, size = 2.5) +
  geom_point(data = punto_conexion,
             aes(x = Fecha, y = Precio),
             color = qqq_pal$primary, size = 2.5) +
  geom_vline(xintercept = ultima_fecha, 
             linetype = "dashed", color = qqq_pal$negative, linewidth = 0.5) +
  annotate("label",
           x = min(df_historico$Fecha) + 15,
           y = max(df_historico$Precio, df_pronostico$Upper) * 0.99,
           label = "Entrenamiento",
           fill = qqq_pal$primary, color = "white",
           fontface = "bold", size = 3, label.padding = unit(0.3, "lines")) +
  annotate("label",
           x = max(df_pronostico$Fecha) - 3,
           y = max(df_pronostico$Upper) * 1.01,
           label = "Pronóstico",
           fill = qqq_pal$secondary, color = "white",
           fontface = "bold", size = 3, label.padding = unit(0.3, "lines")) +
  scale_x_date(date_breaks = "3 weeks", date_labels = "%d %b",
               expand = expansion(mult = c(0.02, 0.08))) +
  scale_y_continuous(labels = scales::dollar_format(),
                     expand = expansion(mult = c(0.02, 0.05))) +
  labs(title = "Pronóstico ARIMA(1,1,1) con Drift",
       subtitle = "QQQ (Nasdaq-100 ETF) | Últimos 100 días + 10 días de pronóstico | IC 95%",
       x = NULL,
       y = "Precio de Cierre (USD)",
       caption = "Línea verde: Datos históricos | Línea cian: Pronóstico | Área sombreada: Intervalo de confianza 95%") +
  theme_QQQ() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

La visualización gráfica del pronóstico proporciona perspectiva intuitiva sobre la trayectoria predicha y su incertidumbre asociada. Los últimos 100 días históricos (aproximadamente 5 meses de actividad de mercado) muestran patrón de volatilidad moderada alrededor de tendencia ascendente: el \(QQQ\) se movió desde aproximadamente 515 en mayo 2025 hasta 599.58 en septiembre 2025, representando ganancia de ~16.4% durante el período. Esta trayectoria alcista justifica la estimación del drift positivo: el modelo reconoce que durante el período de entrenamiento (octubre 2022 - septiembre 2025) existió movimiento sistemático al alza.

El pronóstico de 10 días continúa esta tendencia pero de manera conservadora: en lugar de proyectar aceleración, predice continuación lineal suave con pendiente determinada por el drift estimado. La banda de confianza (área sombreada azul claro) inicia estrecha alrededor del pronóstico puntual y se expande progresivamente, reflejando incertidumbre creciente. Por horizonte h=10 días, el intervalo 95% abarca aproximadamente 571.75 a 637.76, un rango de 66 o ±5.5% alrededor del pronóstico central. Esta amplitud es consistente con volatilidad histórica del \(QQQ\) (~1-1.5% diaria en desviación estándar), sugeriendo que el modelo calibra apropiadamente su incertidumbre (Tsay, 2010).

4.6.3 Evaluación Comparativa: Predicho vs Real

reales <- head(as.numeric(Prueba), 10)
predichos <- as.numeric(pronostico$mean)
fechas_prueba <- head(as.Date(index(Prueba)), 10)

df_evaluacion <- data.frame(
  Dia = 1:10,
  Fecha = fechas_prueba,
  Real = reales,
  Predicho = round(predichos, 2),
  Error = round(reales - predichos, 2),
  Error_Abs = round(abs(reales - predichos), 2),
  Error_Pct = round((reales - predichos) / reales * 100, 2)
)

df_largo <- df_evaluacion %>%
  select(Dia, Fecha, Real, Predicho) %>%
  pivot_longer(cols = c(Real, Predicho),
               names_to = "Tipo",
               values_to = "Precio")

ggplot(df_largo, aes(x = Dia, y = Precio, color = Tipo, shape = Tipo)) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 3) +
  geom_ribbon(data = df_evaluacion,
              aes(x = Dia, y = Predicho,
                  ymin = as.numeric(pronostico$lower),
                  ymax = as.numeric(pronostico$upper)),
              fill = qqq_pal$secondary, alpha = 0.15,
              inherit.aes = FALSE) +
  scale_color_manual(values = c("Real" = qqq_pal$primary, 
                                "Predicho" = qqq_pal$secondary),
                     labels = c("Predicho" = "Pronóstico", "Real" = "Valor Real")) +
  scale_shape_manual(values = c("Real" = 16, "Predicho" = 17),
                     labels = c("Predicho" = "Pronóstico", "Real" = "Valor Real")) +
  scale_x_continuous(breaks = 1:10, labels = paste0("t+", 1:10)) +
  scale_y_continuous(labels = scales::dollar_format()) +
  labs(title = "Evaluación del Pronóstico: Valores Reales vs Predichos",
       subtitle = "QQQ (Nasdaq-100 ETF) | Primeros 10 días del conjunto de prueba",
       x = "Horizonte de Pronóstico",
       y = "Precio de Cierre (USD)",
       color = NULL,
       shape = NULL,
       caption = "Área sombreada: Intervalo de confianza 95%") +
  theme_QQQ() +
  theme(legend.position = "top")

La evaluación comparativa de pronósticos ex-post (sobre el conjunto de prueba retenido) proporciona evidencia directa de capacidad predictiva del modelo en horizonte corto. Los primeros 10 días observados de octubre 2025 revelan patrón interesante: los valores reales exhiben volatilidad que oscila alrededor de la línea de pronóstico, sin divergencia sistemática consistente. Específicamente, durante días 1-7, los valores reales tienden ligeramente por encima del pronóstico (el QQQ alcanzó máximos de 611.44 el día 6), sugiriendo que el drift estimado fue ligeramente conservador. Sin embargo, durante días 8-10, los valores reales caen por debajo del pronóstico (mínimo de 589.50 el día 8), capturando volatilidad exógena que el modelo \(ARIMA\) no puede anticipar.

Este patrón de desviaciones relativamente pequeñas y no-sistemáticas es exactamente lo esperado bajo la hipótesis de mercado eficiente: el modelo captura la tendencia principal (drift) pero no puede predecir shocks exógenos. Nótese que todos los 10 valores reales caen dentro del intervalo de confianza 95%, lo cual validaría calibración correcta de la incertidumbre. Esto es evidencia empírica de que el modelo no solo captura dirección (tendencia alcista), sino también magnitud apropiada de incertidumbre (amplitud del intervalo).

4.6.4 Tabla de Errores por Observación

tabla_errores <- df_evaluacion %>%
  select(Dia, Fecha, Real, Predicho, Error, Error_Pct) %>%
  mutate(
    Fecha = as.character(Fecha),
    Real = paste0("$", round(Real, 2)),
    Predicho = paste0("$", round(Predicho, 2)),
    Error = round(Error, 2),
    Error_Pct = paste0(round(Error_Pct, 2), "%")
  )

kable(tabla_errores,
      caption = "Evaluación del Pronóstico: Errores por Observación",
      align = c("c", "c", "c", "c", "c", "c"),
      col.names = c("Día", "Fecha", "Valor Real", "Pronóstico", "Error (USD)", "Error (%)")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, color = qqq_pal$primary) %>%
  column_spec(3, color = qqq_pal$primary, bold = TRUE) %>%
  column_spec(4, color = qqq_pal$secondary, bold = TRUE) %>%
  column_spec(5, bold = TRUE) %>%
  column_spec(6, bold = TRUE) %>%
  footnote(general = "Error positivo: el modelo subestimó (valor real > pronóstico). Error negativo: el modelo sobreestimó.",
           general_title = "Nota: ")
Evaluación del Pronóstico: Errores por Observación
Día Fecha Valor Real Pronóstico Error (USD) Error (%)
1 2025-10-01 $603.25 $600.71 2.54 0.42%
2 2025-10-02 $605.73 $601.23 4.50 0.74%
3 2025-10-03 $603.18 $601.61 1.57 0.26%
4 2025-10-06 $607.71 $602.11 5.60 0.92%
5 2025-10-07 $604.51 $602.51 2.00 0.33%
6 2025-10-08 $611.44 $602.99 8.45 1.38%
7 2025-10-09 $610.7 $603.41 7.29 1.19%
8 2025-10-10 $589.5 $603.87 -14.37 -2.44%
9 2025-10-13 $602.01 $604.3 -2.29 -0.38%
10 2025-10-14 $598 $604.76 -6.76 -1.13%
Nota:
Error positivo: el modelo subestimó (valor real > pronóstico). Error negativo: el modelo sobreestimó.

El análisis granular de errores por observación individual revela distribución asimétrica pero no-sistemática. Los primeros tres días (1-3 octubre) exhiben errores positivos pequeños (2.54, 4.50, 1.57), indicando subestimación leve pero consistente—el modelo predijo conservadoramente durante esta fase. Los días 4-7 exhiben variación: día 4 tiene error mayor (+5.60 el 06-octubre, coincidiendo con máximo local), días 5-6 muestran errores moderados, día 7 retorna a error moderado (+$7.29).

El quiebre ocurre en día 8 (10-octubre): error de -14.37, representando subestimación grande del valor real de 589.50 contra pronóstico de 603.87. Este es único error negativo significativo en la muestra, coincidiendo con caída de volatilidad en el mercado esa fecha. Sin embargo, nótese que incluso este error de -14.37 permanece dentro del intervalo de confianza: el límite inferior para día 8 es 574.34, y 589.50 > 574.34. Los días 9-10 muestran pequeños errores negativos (-2.29 y -$6.76), regresando a patrón de desviaciones moderadas.

4.6.5 Métricas Finales de Evaluación

MAE <- mean(abs(df_evaluacion$Error))
RMSE <- sqrt(mean(df_evaluacion$Error^2))
MAPE <- mean(abs(df_evaluacion$Error_Pct))
ME <- mean(df_evaluacion$Error)

dentro_IC <- sum(reales >= as.numeric(pronostico$lower) & 
                   reales <= as.numeric(pronostico$upper))
pct_dentro_IC <- dentro_IC / 10 * 100

tabla_metricas <- data.frame(
  Métrica = c("Error Medio (ME)",
              "Error Absoluto Medio (MAE)",
              "Raíz del Error Cuadrático Medio (RMSE)",
              "Error Porcentual Absoluto Medio (MAPE)",
              "Observaciones dentro del IC 95%"),
  Valor = c(paste0("$", round(ME, 2)),
            paste0("$", round(MAE, 2)),
            paste0("$", round(RMSE, 2)),
            paste0(round(MAPE, 2), "%"),
            paste0(dentro_IC, " de 10 (", pct_dentro_IC, "%)")),
  Interpretación = c(
    ifelse(abs(ME) < 1, "Sin sesgo sistemático ✓", 
           ifelse(ME > 0, "Modelo subestima", "Modelo sobreestima")),
    "Error promedio en USD",
    "Penaliza errores grandes",
    "Error relativo al precio",
    ifelse(pct_dentro_IC >= 80, "Intervalos bien calibrados ✓", 
           "Intervalos pueden estar mal calibrados")
  )
)

kable(tabla_metricas,
      caption = "Métricas de Evaluación del Pronóstico - Datos de Prueba",
      align = c("l", "c", "l")) %>%
  kable_styling(bootstrap_options = c("hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, background = qqq_pal$primary, color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, color = qqq_pal$primary) %>%
  column_spec(2, bold = TRUE) %>%
  row_spec(5, background = "#e8f5e9", color = qqq_pal$text_dark, bold = TRUE)
Métricas de Evaluación del Pronóstico - Datos de Prueba
Métrica Valor Interpretación
Error Medio (ME) $0.85 Sin sesgo sistemático ✓
Error Absoluto Medio (MAE) $5.54 Error promedio en USD
Raíz del Error Cuadrático Medio (RMSE) $6.68 Penaliza errores grandes
Error Porcentual Absoluto Medio (MAPE) 0.92% Error relativo al precio
Observaciones dentro del IC 95% 10 de 10 (100%) Intervalos bien calibrados ✓

Las métricas de evaluación agregadas sintetizan el desempeño del modelo \(ARIMA(1,1,1)\) + drift sobre el horizonte de 10 días de prueba. El Error Medio (ME) = 0.85 es notablemente pequeño, prácticamente cero en términos prácticos (0.14% del precio promedio observado de ~604). Esto valida que el modelo no contiene sesgo direccional sistemático: no subestima ni sobrestima persistentemente. La acumulación de errores positivos y negativos se cancela, produciendo \(ME\) cercano a cero—característica fundamental de un modelo bien especificado.

El Error Absoluto Medio (MAE) = 5.54 cuantifica la magnitud promedio de desviación: en promedio, las predicciones erraron por aproximadamente 5.54, o 0.92% del precio. Para un ETF tecnológico volátil como el \(QQQ\), este nivel de error es pequeño aunque no negligible. En contexto de inversión, error de ~5.54 en predicción de precio de 604 representa precisión suficiente para identificar dirección general pero insuficiente para timing exacto. La Raíz del Error Cuadrático Medio (RMSE) = 6.68 es ligeramente superior a \(MAE\) porque penaliza desviaciones grandes: el error máximo observado (-14.37 en día 8) elevado al cuadrado domina el cálculo de \(RMSE\).

El Error Porcentual Absoluto Medio (MAPE) = 0.92% expresa error en términos relativos: en promedio, predicciones erraron por menos de 1% del valor real. Este es un estándar de desempeño excelente para pronóstico de precios de activos financieros, donde volatilidad diaria típica es 1-2% (Hyndman & Athanasopoulos, 2021). La métrica más reveladora es “Observaciones dentro del IC 95%” = 10 de 10 (100%). Esta perfecta cobertura indica que el intervalo de confianza fue exactamente bien calibrado: todos los valores reales cayeron dentro de las bandas predichas, ni más ni menos que lo esperado teóricamente. Si el modelo fuese mal especificado, se esperaría que algunos valores reales cayesen fuera del intervalo; el hecho de que todos caigan dentro valida la calibración correcta de la incertidumbre (Hyndman & Koehler, 2006).

9. Conclusiones

9.1 Hallazgos Principales

[PLACEHOLDER: Resumen de hallazgos principales del análisis ARIMA]

9.2 Implicaciones Prácticas

[PLACEHOLDER: Implicaciones de los pronósticos para inversionistas y analistas de mercado]

9.3 Limitaciones del Análisis

[PLACEHOLDER: Limitaciones del modelo y aspectos no capturados]

9.4 Recomendaciones Futuras

[PLACEHOLDER: Sugerencias para mejoras y extensiones del análisis]


Bibliografía

Box, G. E. P., & Jenkins, G. M. (1976). Time series analysis: Forecasting and control (2nd ed.). Holden-Day.

Brockwell, P. J., & Davis, R. A. (2016). Introduction to time series and forecasting (3rd ed.). Springer.

Chatfield, C. (2000). Time-series forecasting. Chapman and Hall/CRC.

Dickey, D. A., & Fuller, W. A. (1979). Distribution of the estimators for autoregressive time series with a unit root. Journal of the American Statistical Association, 74(366), 427–431.

Hamilton, J. D. (1994). Time series analysis. Princeton University Press.

Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: principles and practice (3rd ed.). OTexts. https://otexts.com/fpp3/

Jarque, C. M., & Bera, A. K. (1987). A test for normality of observations and regression residuals. International Statistical Review, 55(2), 163–172.

Ljung, G. M., & Box, G. E. P. (1978). On a measure of lack of fit in time series models. Biometrika, 65(2), 297–303.

Tsay, R. S. (2010). Analysis of financial time series (3rd ed.). John Wiley & Sons.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ASIGNATURA: Gestión de Datos
PROFESOR: Orlando Joaqui-Barandica
UNIVERSIDAD: Universidad del Valle
FACULTAD: Facultad de Ingeniería
PROGRAMA: Ingeniería Industrial
ESTUDIANTE: Camilo
FECHA ENTREGA:
VERSIÓN: 1.0
Documento generado con R Markdown | Tema: Series de Tiempo y Pronósticos ARIMA
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━